import AppKit
import AXorcist
import Commander
import CoreGraphics
import Foundation
import PeekabooCore
import PeekabooFoundation

/// Perform drag and drop operations using intelligent element finding
@available(macOS 14.0, *)
@MainActor
struct DragCommand: ErrorHandlingCommand, OutputFormattable {
    @Option(help: "Starting element ID from session")
    var from: String?

    @Option(help: "Starting coordinates as 'x,y'")
    var fromCoords: String?

    @Option(help: "Target element ID from session")
    var to: String?

    @Option(help: "Target coordinates as 'x,y'")
    var toCoords: String?

    @Option(help: "Target application (e.g., 'Trash', 'Finder')")
    var toApp: String?

    @Option(help: "Session ID for element resolution")
    var session: String?

    @Option(help: "Duration of drag in milliseconds (default: 500)")
    var duration: Int?

    @Option(help: "Number of intermediate steps (default: 20)")
    var steps: Int?

    @Option(help: "Modifier keys to hold during drag (comma-separated: cmd,shift,option,ctrl)")
    var modifiers: String?

    @Option(help: "Movement profile (linear or human)")
    var profile: String?
    @OptionGroup var focusOptions: FocusCommandOptions
    @RuntimeStorage private var runtime: CommandRuntime?

    private var resolvedRuntime: CommandRuntime {
        guard let runtime else {
            preconditionFailure("CommandRuntime must be configured before accessing runtime resources")
        }
        return runtime
    }

    private var services: any PeekabooServiceProviding { self.resolvedRuntime.services }
    private var logger: Logger { self.resolvedRuntime.logger }
    var outputLogger: Logger { self.logger }
    var jsonOutput: Bool { self.resolvedRuntime.configuration.jsonOutput }

    @MainActor
    mutating func run(using runtime: CommandRuntime) async throws {
        self.runtime = runtime
        self.logger.setJsonOutputMode(self.jsonOutput)
        let startTime = Date()

        do {
            try self.validateInputs()

            let sessionId = try await self.resolveSession()
            if let sessionId {
                try await ensureFocused(
                    sessionId: sessionId,
                    options: self.focusOptions,
                    services: self.services
                )
            }

            let startPoint = try await self.resolvePoint(
                elementId: self.from,
                coords: self.fromCoords,
                sessionId: sessionId,
                description: "from"
            )

            let endPoint: CGPoint = if let targetApp = toApp {
                try await self.findApplicationPoint(targetApp)
            } else {
                try await self.resolvePoint(
                    elementId: self.to,
                    coords: self.toCoords,
                    sessionId: sessionId,
                    description: "to"
                )
            }

            let distance = hypot(endPoint.x - startPoint.x, endPoint.y - startPoint.y)
            let profileSelection = CursorMovementProfileSelection(
                rawValue: (self.profile ?? "linear").lowercased()
            ) ?? .linear
            let movement = CursorMovementResolver.resolve(
                selection: profileSelection,
                durationOverride: self.duration,
                stepsOverride: self.steps,
                baseSmooth: true,
                distance: distance,
                defaultDuration: 500,
                defaultSteps: 20
            )

            let dragRequest = DragRequest(
                from: startPoint,
                to: endPoint,
                duration: movement.duration,
                steps: movement.steps,
                modifiers: self.modifiers,
                profile: movement.profile
            )
            try await AutomationServiceBridge.drag(automation: self.services.automation, request: dragRequest)
            AutomationEventLogger.log(
                .drag,
                "drag from=(\(Int(startPoint.x)),\(Int(startPoint.y))) to=(\(Int(endPoint.x)),\(Int(endPoint.y))) "
                    + "modifiers=\(self.modifiers ?? "none") session=\(sessionId ?? "latest") "
                    + "profile=\(movement.profileName)"
            )

            try await Task.sleep(nanoseconds: 100_000_000)

            let result = DragResult(
                success: true,
                from: ["x": Int(startPoint.x), "y": Int(startPoint.y)],
                to: ["x": Int(endPoint.x), "y": Int(endPoint.y)],
                duration: movement.duration,
                steps: movement.steps,
                profile: movement.profileName,
                modifiers: self.modifiers ?? "none",
                executionTime: Date().timeIntervalSince(startTime)
            )

            output(result) {
                print("✅ Drag successful")
                print("📍 From: (\(Int(startPoint.x)), \(Int(startPoint.y)))")
                print("📍 To: (\(Int(endPoint.x)), \(Int(endPoint.y)))")
                print("🧭 Profile: \(movement.profileName.capitalized)")
                print("⏱️  Duration: \(movement.duration)ms with \(movement.steps) steps")
                if let mods = modifiers {
                    print("⌨️  Modifiers: \(mods)")
                }
                print("⏱️  Completed in \(String(format: "%.2f", Date().timeIntervalSince(startTime)))s")
            }
        } catch {
            self.handleError(error)
            throw ExitCode.failure
        }
    }

    // Validate user input combinations
    private func validateInputs() throws {
        guard self.from != nil || self.fromCoords != nil else {
            throw ValidationError("Must specify either --from or --from-coords")
        }

        guard self.to != nil || self.toCoords != nil || self.toApp != nil else {
            throw ValidationError("Must specify either --to, --to-coords, or --to-app")
        }

        if self.to != nil || self.toCoords != nil {
            guard (self.to != nil) != (self.toCoords != nil) else {
                throw ValidationError("Specify only one of --to or --to-coords")
            }
        }

        if self.from != nil && self.fromCoords != nil {
            throw ValidationError("Specify only one of --from or --from-coords")
        }

        if let profileName = self.profile?.lowercased(),
           CursorMovementProfileSelection(rawValue: profileName) == nil {
            throw ValidationError("Invalid profile '\(profileName)'. Use 'linear' or 'human'.")
        }
    }

    private func resolveSession() async throws -> String? {
        if let provided = self.session {
            return provided
        }
        return await self.services.sessions.getMostRecentSession()
    }

    private func resolvePoint(
        elementId: String?,
        coords: String?,
        sessionId: String?,
        description: String
    ) async throws -> CGPoint {
        if let coordinateString = coords {
            let components = coordinateString.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) }
            guard components.count == 2,
                  let x = Double(components[0]),
                  let y = Double(components[1])
            else {
                throw ValidationError("Invalid coordinates format: '\(coordinateString)'. Expected 'x,y'")
            }
            return CGPoint(x: x, y: y)
        }

        guard let element = elementId else {
            throw ValidationError("No \(description) point specified")
        }

        guard let sessionId else {
            throw ValidationError("Session ID required when using element IDs")
        }

        let target = ClickTarget.elementId(element)
        let waitResult = try await AutomationServiceBridge.waitForElement(
            automation: self.services.automation,
            target: target,
            timeout: 5.0,
            sessionId: sessionId
        )

        guard waitResult.found, let foundElement = waitResult.element else {
            throw PeekabooError.elementNotFound("Element with ID '\(element)' not found")
        }

        return CGPoint(
            x: foundElement.bounds.origin.x + foundElement.bounds.width / 2,
            y: foundElement.bounds.origin.y + foundElement.bounds.height / 2
        )
    }

    private func findApplicationPoint(_ appName: String) async throws -> CGPoint {
        if appName.lowercased() == "trash" {
            return try await self.findTrashPoint()
        }

        let appInfo = try await self.resolveApplication(appName, services: self.services)

        return try await Task { @MainActor in
            guard let runningApp = NSRunningApplication(processIdentifier: appInfo.processIdentifier) else {
                throw PeekabooError.appNotFound(appName)
            }

            let axApp = AXApp(runningApp)
            guard let windowElement = axApp.element.focusedWindow() ?? axApp.element.windows()?.first else {
                throw PeekabooError.windowNotFound(
                    criteria: "No accessible window for \(appInfo.name)"
                )
            }

            guard let frame = windowElement.frame() else {
                throw PeekabooError.windowNotFound(
                    criteria: "Window bounds unavailable for \(appInfo.name)"
                )
            }

            return CGPoint(x: frame.midX, y: frame.midY)
        }.value
    }

    private func findTrashPoint() async throws -> CGPoint {
        guard let dock = await self.findDockApplication(),
              let list = dock.children()?.first(where: { $0.role() == "AXList" })
        else {
            throw PeekabooError.elementNotFound("Dock not found")
        }

        let items = list.children() ?? []
        if let trash = items.first(where: { $0.label()?.lowercased() == "trash" }) {
            if let position = trash.position(), let size = trash.size() {
                return CGPoint(x: position.x + size.width / 2, y: position.y + size.height / 2)
            }
        }

        throw PeekabooError.elementNotFound("Trash not found in Dock")
    }

    private func findDockApplication() async -> Element? {
        await MainActor.run {
            let apps = NSWorkspace.shared.runningApplications
            guard let dockApp = apps.first(where: { $0.bundleIdentifier == "com.apple.dock" }) else {
                return nil
            }
            return AXApp(dockApp).element
        }
    }
}

// MARK: - Output Types

private struct DragResult: Codable {
    let success: Bool
    let from: [String: Int]
    let to: [String: Int]
    let duration: Int
    let steps: Int
    let profile: String
    let modifiers: String
    let executionTime: TimeInterval
}

// MARK: - Conformances

@MainActor
extension DragCommand: ParsableCommand {
    nonisolated(unsafe) static var commandDescription: CommandDescription {
        MainActorCommandDescription.describe {
            CommandDescription(
                commandName: "drag",
                abstract: "Perform drag and drop operations",
                discussion: """
                Execute click-and-drag operations for moving elements, selecting text, or dragging files.

                EXAMPLES:
                  peekaboo drag --from B1 --to T2
                  peekaboo drag --from-coords "100,200" --to-coords "400,300"
                  peekaboo drag --from B1 --to-app Trash
                  peekaboo drag --from S1 --to-coords "500,250" --duration 2000
                  peekaboo drag --from T1 --to T5 --modifiers shift
                """,
                version: "2.0.0",
                showHelpOnEmptyInvocation: true
            )
        }
    }
}

extension DragCommand: AsyncRuntimeCommand {}

@MainActor
extension DragCommand: CommanderBindableCommand {
    mutating func applyCommanderValues(_ values: CommanderBindableValues) throws {
        self.from = values.singleOption("from")
        self.fromCoords = values.singleOption("fromCoords")
        self.to = values.singleOption("to")
        self.toCoords = values.singleOption("toCoords")
        self.toApp = values.singleOption("toApp")
        self.session = values.singleOption("session")
        if let duration: Int = try values.decodeOption("duration", as: Int.self) {
            self.duration = duration
        }
        if let steps: Int = try values.decodeOption("steps", as: Int.self) {
            self.steps = steps
        }
        self.modifiers = values.singleOption("modifiers")
        self.profile = values.singleOption("profile")
        self.focusOptions = try values.makeFocusOptions()
    }
}

extension DragCommand: ApplicationResolver {}
